3891d6d0e31cec07a904d8ca5edb6593e5fa96ab
2 * These plugins provide extra functionality for interaction with textareas.
4 * - encapsulateSelection: Ported from skins/common/edit.js by Trevor Parscal
5 * © 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
6 * - getCaretPosition, scrollToCaretPosition: Ported from Wikia's LinkSuggest extension
7 * https://github.com/Wikia/app/blob/c0cd8b763/extensions/wikia/LinkSuggest/js/jquery.wikia.linksuggest.js
8 * © 2010 Inez Korczyński (korczynski@gmail.com) & Jesús Martínez Novo (martineznovo@gmail.com) (GPLv2)
12 * @class jQuery.plugin.textSelection
14 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
16 * var $textbox = $( '#wpTextbox1' );
17 * $textbox.textSelection( 'setContents', 'This is bold!' );
18 * $textbox.textSelection( 'setSelection', { start: 8, end: 12 } );
19 * $textbox.textSelection( 'encapsulateSelection', { pre: '<b>', post: '</b>' } );
20 * // Result: Textbox contains 'This is <b>bold</b>!', with cursor before the '!'
24 * Do things to the selection in a `<textarea>`, or a textarea-like editable element.
26 * var $textbox = $( '#wpTextbox1' );
27 * $textbox.textSelection( 'setContents', 'This is bold!' );
28 * $textbox.textSelection( 'setSelection', { start: 8, end: 12 } );
29 * $textbox.textSelection( 'encapsulateSelection', { pre: '<b>', post: '</b>' } );
30 * // Result: Textbox contains 'This is <b>bold</b>!', with cursor before the '!'
32 * @param {string} command Command to execute, one of:
34 * - {@link jQuery.plugin.textSelection#getContents getContents}
35 * - {@link jQuery.plugin.textSelection#setContents setContents}
36 * - {@link jQuery.plugin.textSelection#getSelection getSelection}
37 * - {@link jQuery.plugin.textSelection#encapsulateSelection encapsulateSelection}
38 * - {@link jQuery.plugin.textSelection#getCaretPosition getCaretPosition}
39 * - {@link jQuery.plugin.textSelection#setSelection setSelection}
40 * - {@link jQuery.plugin.textSelection#scrollToCaretPosition scrollToCaretPosition}
41 * - {@link jQuery.plugin.textSelection#register register}
42 * - {@link jQuery.plugin.textSelection#unregister unregister}
43 * @param {Mixed} [options] Options to pass to the command
44 * @return {Mixed} Depending on the command
46 $.fn
.textSelection = function ( command
, options
) {
53 * Get the contents of the textarea.
58 getContents: function () {
63 * Set the contents of the textarea, replacing anything that was there before.
66 * @param {string} content
68 setContents: function ( content
) {
73 * Get the currently selected text in this textarea.
78 getSelection: function () {
82 if ( !el
|| $( el
).is( ':hidden' ) ) {
84 } else if ( el
.selectionStart
|| el
.selectionStart
=== 0 ) {
85 retval
= el
.value
.substring( el
.selectionStart
, el
.selectionEnd
);
92 * Insert text at the beginning and end of a text selection, optionally
93 * inserting text at the caret when selection is empty.
95 * Also focusses the textarea.
98 * @param {Object} [options]
99 * @param {string} [options.pre] Text to insert before the cursor/selection
100 * @param {string} [options.peri] Text to insert between pre and post and select afterwards
101 * @param {string} [options.post] Text to insert after the cursor/selection
102 * @param {boolean} [options.ownline=false] Put the inserted text on a line of its own
103 * @param {boolean} [options.replace=false] If there is a selection, replace it with peri
104 * instead of leaving it alone
105 * @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted (but not
106 * if there was a selection and replace==false, or if splitlines==true)
107 * @param {boolean} [options.splitlines=false] If multiple lines are selected, encapsulate
108 * each line individually
109 * @param {number} [options.selectionStart] Position to start selection at
110 * @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at
114 encapsulateSelection: function ( options
) {
115 return this.each( function () {
116 var selText
, scrollTop
, insertText
,
117 isSample
, startPos
, endPos
,
123 * Check if the selected text is the same as the insert text
125 function checkSelectedText() {
127 selText
= options
.peri
;
129 } else if ( options
.replace
) {
130 selText
= options
.peri
;
132 while ( selText
.charAt( selText
.length
- 1 ) === ' ' ) {
133 // Exclude ending space char
134 selText
= selText
.slice( 0, -1 );
137 while ( selText
.charAt( 0 ) === ' ' ) {
138 // Exclude prepending space char
139 selText
= selText
.slice( 1 );
147 * Do the splitlines stuff.
149 * Wrap each line of the selected text with pre and post
151 * @param {string} selText Selected text
152 * @param {string} pre Text before
153 * @param {string} post Text after
154 * @return {string} Wrapped text
156 function doSplitLines( selText
, pre
, post
) {
159 selTextArr
= selText
.split( '\n' );
160 for ( i
= 0; i
< selTextArr
.length
; i
++ ) {
161 insertText
+= pre
+ selTextArr
[ i
] + post
;
162 if ( i
!== selTextArr
.length
- 1 ) {
170 // Do nothing if display none
171 if ( this.style
.display
!== 'none' ) {
172 if ( this.selectionStart
|| this.selectionStart
=== 0 ) {
174 if ( options
.selectionStart
!== undefined ) {
175 $( this ).textSelection( 'setSelection', { start
: options
.selectionStart
, end
: options
.selectionEnd
} );
178 selText
= $( this ).textSelection( 'getSelection' );
179 startPos
= this.selectionStart
;
180 endPos
= this.selectionEnd
;
181 scrollTop
= this.scrollTop
;
184 options
.selectionStart
!== undefined &&
185 endPos
- startPos
!== options
.selectionEnd
- options
.selectionStart
187 // This means there is a difference in the selection range returned by browser and what we passed.
188 // This happens for Chrome in the case of composite characters. Ref bug #30130
189 // Set the startPos to the correct position.
190 startPos
= options
.selectionStart
;
193 insertText
= pre
+ selText
+ post
;
194 if ( options
.splitlines
) {
195 insertText
= doSplitLines( selText
, pre
, post
);
197 if ( options
.ownline
) {
198 if ( startPos
!== 0 && this.value
.charAt( startPos
- 1 ) !== '\n' && this.value
.charAt( startPos
- 1 ) !== '\r' ) {
199 insertText
= '\n' + insertText
;
202 if ( this.value
.charAt( endPos
) !== '\n' && this.value
.charAt( endPos
) !== '\r' ) {
207 this.value
= this.value
.slice( 0, startPos
) + insertText
+
208 this.value
.slice( endPos
);
209 // Setting this.value scrolls the textarea to the top, restore the scroll position
210 this.scrollTop
= scrollTop
;
211 if ( window
.opera
) {
212 pre
= pre
.replace( /\r?\n/g, '\r\n' );
213 selText
= selText
.replace( /\r?\n/g, '\r\n' );
214 post
= post
.replace( /\r?\n/g, '\r\n' );
216 if ( isSample
&& options
.selectPeri
&& ( !options
.splitlines
|| ( options
.splitlines
&& selText
.indexOf( '\n' ) === -1 ) ) ) {
217 this.selectionStart
= startPos
+ pre
.length
;
218 this.selectionEnd
= startPos
+ pre
.length
+ selText
.length
;
220 this.selectionStart
= startPos
+ insertText
.length
;
221 this.selectionEnd
= this.selectionStart
;
225 $( this ).trigger( 'encapsulateSelection', [ options
.pre
, options
.peri
, options
.post
, options
.ownline
,
226 options
.replace
, options
.splitlines
] );
231 * Get the current cursor position (in UTF-16 code units) in a textarea.
234 * @param {Object} [options]
235 * @param {Object} [options.startAndEnd=false] Return range of the selection rather than just start
237 * - When `startAndEnd` is `false`: number
238 * - When `startAndEnd` is `true`: array with two numbers, for start and end of selection
240 getCaretPosition: function ( options
) {
241 function getCaret( e
) {
245 if ( e
&& ( e
.selectionStart
|| e
.selectionStart
=== 0 ) ) {
246 caretPos
= e
.selectionStart
;
247 endPos
= e
.selectionEnd
;
249 return options
.startAndEnd
? [ caretPos
, endPos
] : caretPos
;
251 return getCaret( this.get( 0 ) );
255 * Set the current cursor position (in UTF-16 code units) in a textarea.
258 * @param {Object} [options]
259 * @param {number} options.start
260 * @param {number} [options.end=options.start]
264 setSelection: function ( options
) {
265 return this.each( function () {
266 // Do nothing if hidden
267 if ( !$( this ).is( ':hidden' ) ) {
268 if ( this.selectionStart
|| this.selectionStart
=== 0 ) {
269 // Opera 9.0 doesn't allow setting selectionStart past
270 // selectionEnd; any attempts to do that will be ignored
271 // Make sure to set them in the right order
272 if ( options
.start
> this.selectionEnd
) {
273 this.selectionEnd
= options
.end
;
274 this.selectionStart
= options
.start
;
276 this.selectionStart
= options
.start
;
277 this.selectionEnd
= options
.end
;
285 * Scroll a textarea to the current cursor position. You can set the cursor
286 * position with #setSelection.
289 * @param {Object} [options]
290 * @param {string} [options.force=false] Whether to force a scroll even if the caret position
291 * is already visible.
295 scrollToCaretPosition: function ( options
) {
296 function getLineLength( e
) {
297 return Math
.floor( e
.scrollWidth
/ ( $.client
.profile().platform
=== 'linux' ? 7 : 8 ) );
299 function getCaretScrollPosition( e
) {
300 // FIXME: This functions sucks and is off by a few lines most
301 // of the time. It should be replaced by something decent.
304 text
= e
.value
.replace( /\r/g, '' ),
305 caret
= $( e
).textSelection( 'getCaretPosition' ),
306 lineLength
= getLineLength( e
),
311 for ( i
= 0; i
< caret
; i
++ ) {
313 if ( text
.charAt( i
) === ' ' ) {
314 lastSpaceInLine
= charInLine
;
315 } else if ( text
.charAt( i
) === '\n' ) {
320 if ( charInLine
> lineLength
) {
321 if ( lastSpaceInLine
> 0 ) {
322 charInLine
= charInLine
- lastSpaceInLine
;
329 for ( j
= caret
; j
< caret
+ lineLength
; j
++ ) {
331 text
.charAt( j
) === ' ' ||
332 text
.charAt( j
) === '\n' ||
333 caret
=== text
.length
339 if ( nextSpace
> lineLength
&& caret
<= lineLength
) {
340 charInLine
= caret
- lastSpaceInLine
;
343 return ( $.client
.profile().platform
=== 'mac' ? 13 : ( $.client
.profile().platform
=== 'linux' ? 15 : 16 ) ) * row
;
345 return this.each( function () {
347 // Do nothing if hidden
348 if ( !$( this ).is( ':hidden' ) ) {
349 if ( this.selectionStart
|| this.selectionStart
=== 0 ) {
350 scroll
= getCaretScrollPosition( this );
351 if ( options
.force
|| scroll
< $( this ).scrollTop() ||
352 scroll
> $( this ).scrollTop() + $( this ).height() ) {
353 $( this ).scrollTop( scroll
);
357 $( this ).trigger( 'scrollToPosition' );
365 * Register an alternative textSelection API for this element.
368 * @param {Object} functions Functions to replace. Keys are command names (as in #textSelection,
369 * except 'register' and 'unregister'). Values are functions to execute when a given command is
376 * Unregister the alternative textSelection API for this element (see #register).
381 alternateFn
= $( this ).data( 'jquery.textSelection' );
385 // case 'getContents': // no params
386 // case 'setContents': // no params with defaults
387 // case 'getSelection': // no params
388 case 'encapsulateSelection':
389 options
= $.extend( {
397 selectionStart
: undefined,
398 selectionEnd
: undefined
401 case 'getCaretPosition':
402 options
= $.extend( {
407 options
= $.extend( {
411 if ( options
.end
=== undefined ) {
412 options
.end
= options
.start
;
415 case 'scrollToCaretPosition':
416 options
= $.extend( {
422 throw new Error( 'Another textSelection API was already registered' );
424 $( this ).data( 'jquery.textSelection', options
);
425 // No need to update alternateFn as this command only stores the options.
426 // A command that uses it will set it again.
429 $( this ).removeData( 'jquery.textSelection' );
433 retval
= ( alternateFn
&& alternateFn
[ command
] || fn
[ command
] ).call( this, options
);
442 * @method textSelection
443 * @inheritdoc jQuery.plugin.textSelection#textSelection